// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controller

import (
	"context"
	"fmt"
	"time"

	v1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
	listerv1 "k8s.io/client-go/listers/core/v1"
	"k8s.io/client-go/tools/cache"

	"istio.io/istio/pkg/kube"
	"istio.io/istio/pkg/kube/inject"
	"istio.io/istio/pkg/queue"

	// "k8s.io/client-go/util/workqueue"
	// "istio.io/istio/security/pkg/k8s"
	"ycontroller/pkg/k8s"

	"istio.io/pkg/log"
	// log "k8s.io/klog/v2"
	// "k8s.io/client-go/kubernetes"
	// "k8s.io/apimachinery/pkg/labels"
	// "k8s.io/apimachinery/pkg/selection"
)

const (
	// NamespaceResyncPeriod : every NamespaceResyncPeriod, namespaceUpdated() will be invoked
	// for every namespace. This value must be configured so Citadel
	// can update its CA certificate in a ConfigMap in every namespace.
	NamespaceResyncPeriod = time.Second * 60
	// CACertNamespaceConfigMap is the name of the ConfigMap in each namespace storing the root cert of non-Kube CA.
	// CACertNamespaceConfigMap = "start-tomcat"
)

var (
	configMapLabel      = map[string]string{"yhf.99bill.com/config": "true"}
	configMapAnnotation = map[string]string{"yhf.99biil.com/alert": "Don't modify manually, This object managed by ycontroller"}
)

// NamespaceController manages reconciles a configmap in each namespace with a desired set of data.
type NamespaceController struct {
	// getData is the function to fetch the data we will insert into the config map
	getData       map[string]string
	namespace     string
	configmapName string
	selectorKey   string
	client        corev1.CoreV1Interface

	queue              queue.Instance
	namespacesInformer cache.SharedInformer
	configMapInformer  cache.SharedInformer
	namespaceLister    listerv1.NamespaceLister
	configmapLister    listerv1.ConfigMapLister
}

// NewNamespaceController returns a pointer to a newly constructed NamespaceController instance.
func NewNamespaceController(args map[string]string, data map[string]string, kubeClient kube.Client) *NamespaceController {
	c := &NamespaceController{
		getData:       data,
		namespace:     args["namespace"],
		configmapName: args["configmapName"],
		selectorKey:   args["selectorKey"],
		client:        kubeClient.CoreV1(),
		queue:         queue.NewQueue(time.Second),
	}

	c.configMapInformer = kubeClient.KubeInformer().Core().V1().ConfigMaps().Informer()
	c.configmapLister = kubeClient.KubeInformer().Core().V1().ConfigMaps().Lister()
	c.namespacesInformer = kubeClient.KubeInformer().Core().V1().Namespaces().Informer()
	c.namespaceLister = kubeClient.KubeInformer().Core().V1().Namespaces().Lister()

	c.configMapInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		UpdateFunc: func(_, obj interface{}) {
			cm, err := convertToConfigMap(obj)
			if err != nil {
				log.Errorf("failed to convert to configmap: %v", err)
				return
			}
			// This is a change to a configmap we don't watch, ignore it
			if cm.Name != c.configmapName {
				return
			}

			if cm.Namespace == c.namespace {
				c.updateControllerData(cm)
				c.queue.Push(func() error {
					return c.sourceConfigMapUpdate(cm)
				})
				return
			}

			c.queue.Push(func() error {
				return c.configMapChange(cm)
			})
		},
		DeleteFunc: func(obj interface{}) {
			cm, err := convertToConfigMap(obj)
			if err != nil {
				log.Errorf("failed to convert to configmap: %v", err)
				return
			}
			// This is a change to a configmap we don't watch, ignore it
			if cm.Name != c.configmapName {
				return
			}

			if cm.Namespace == c.namespace {
				c.sourceConfigMapDelete(cm)
				return
			}

			c.queue.Push(func() error {
				ns, err := c.namespaceLister.Get(cm.Namespace)
				if err != nil {
					// namespace is deleted before
					if apierrors.IsNotFound(err) {
						return nil
					}
					return err
				}
				// If the namespace is terminating, we may get into a loop of trying to re-add the configmap back
				// We should make sure the namespace still exists
				if ns.Status.Phase != v1.NamespaceTerminating {
					return c.insertDataForNamespace(cm.Namespace)
				}
				return nil
			})
		},
	})

	c.namespacesInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			c.queue.Push(func() error {
				return c.namespaceChange(obj.(*v1.Namespace))
			})
		},
		UpdateFunc: func(_, obj interface{}) {
			c.queue.Push(func() error {
				return c.namespaceChange(obj.(*v1.Namespace))
			})
		},
	})

	return c
}

// Run starts the NamespaceController until a value is sent to stopCh.
func (nc *NamespaceController) Run(stopCh <-chan struct{}) {
	if !cache.WaitForCacheSync(stopCh, nc.namespacesInformer.HasSynced, nc.configMapInformer.HasSynced) {
		log.Error("Failed to sync namespace controller cache")
	}
	log.Infof("Namespace controller started")
	go nc.queue.Run(stopCh)
}

// insertDataForNamespace will add data into the configmap for the specified namespace
// If the configmap is not found, it will be created.
// If you know the current contents of the configmap, using UpdateDataInConfigMap is more efficient.
func (nc *NamespaceController) insertDataForNamespace(ns string) error {
	meta := metav1.ObjectMeta{
		Name:        nc.configmapName,
		Namespace:   ns,
		Labels:      configMapLabel,
		Annotations: configMapAnnotation,
	}
	log.Infof("namespace %s insertDataForNamespace configmap %s", ns, nc.configmapName)
	return k8s.InsertDataToConfigMap(nc.client, nc.configmapLister, meta, nc.getData)
}

// On namespace change, update the config map.
// If terminating, this will be skipped
func (nc *NamespaceController) namespaceChange(ns *v1.Namespace) error {
	// skip special kubernetes system namespaces
	for _, namespace := range inject.IgnoredNamespaces {
		if ns.Name == namespace {
			return nil
		}
	}

	selectorKeyFlag := false
	for k := range ns.ObjectMeta.Labels {
		if k == nc.selectorKey {
			selectorKeyFlag = true
		}
	}
	if !selectorKeyFlag {
		return nil
	}

	if ns.Status.Phase != v1.NamespaceTerminating {
		return nc.insertDataForNamespace(ns.Name)
	}
	return nil
}

// When a config map is changed, merge the data into the configmap
func (nc *NamespaceController) configMapChange(cm *v1.ConfigMap) error {

	if cm.ObjectMeta.Namespace == nc.namespace {
		return nil
	}

	if err := k8s.UpdateDataInConfigMap(nc.client, cm.DeepCopy(), nc.getData); err != nil {
		return fmt.Errorf("error when inserting new data to configmap %v: %v", cm.Name, err)
	}
	return nil
	// return k8s.UpdateDataInConfigMap(nc.client, cm.DeepCopy(), nc.getData)
}

func convertToConfigMap(obj interface{}) (*v1.ConfigMap, error) {
	cm, ok := obj.(*v1.ConfigMap)
	if !ok {
		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			return nil, fmt.Errorf("couldn't get object from tombstone %#v", obj)
		}
		cm, ok = tombstone.Obj.(*v1.ConfigMap)
		if !ok {
			return nil, fmt.Errorf("tombstone contained object that is not a ConfigMap %#v", obj)
		}
	}
	return cm, nil
}

// sourceConfigMapUpdate for the change of configmap of the source namespace
// to update the data of all the need namespace
func (nc *NamespaceController) sourceConfigMapUpdate(cm *v1.ConfigMap) error {
	// ns, err := labels.NewRequirement(nc.selectorKey, selection.Exists, []string{})
	ps := metav1.LabelSelector{
		MatchLabels: map[string]string{
			nc.selectorKey: "true",
		},
		// MatchExpressions: ns,
	}
	ns, err := metav1.LabelSelectorAsSelector(&ps)
	if err != nil {
		return err
	}

	needNameSpaces, err := nc.namespaceLister.List(ns)
	if err != nil {
		return err
	}
	for _, n := range needNameSpaces {
		if n.Namespace == nc.namespace {
			continue
		}

		configMapInNamespace, err := nc.client.ConfigMaps(n.Name).Get(context.TODO(), nc.configmapName, metav1.GetOptions{})
		if err != nil {
			return err
		}
		if err := k8s.UpdateDataInConfigMap(nc.client, configMapInNamespace.DeepCopy(), cm.Data); err != nil {
			return fmt.Errorf("Positive new source data to configmap: %v", cm.Namespace)
		}

	}

	return nil
}

func (nc *NamespaceController) sourceConfigMapDelete(cm *v1.ConfigMap) {
	return
}

func (nc *NamespaceController) updateControllerData(cm *v1.ConfigMap) {
	if cm.Data == nil {
		return
	}

	newData := cm.Data

	if nc.getData == nil {
		nc.getData = newData
		return
	}

	nc.getData = cm.Data

}
